你已經走到 Day 25。昨天 Day 24,我們拿五個人工 DNA 位置,替同一個人工變異準備 REF 和 ALT 兩條曲線,逐點計算差異。現在試著想像:研究員拿到一串數字,最想做的事情大概是「先把它畫出來」。但如果橫軸差了一格、兩條線的單位不同,畫得再美也沒有意義。
今天的挑戰,就是把 Day 24 已驗證的人工 JSON 變成一張可以拿著數字逐點核對的 SVG 科學圖。我們保留你最初設計的學習方法:Google AI Studio 先做出看得見的 MVP → 在 VS Code 用 Python 逆向拆解每一步。這樣讀者不必面對一個空白編輯器,也能從現成圖形反問:「畫這條線究竟要哪幾行程式?」
Day 24 保存的人工資料如下,變異 ID chr1:110:A>G 僅為格式練習,未經 GRCh38 REF 驗證:
| 1-based 位置 | REF proxy | ALT proxy | Delta = ALT − REF | 繪圖觀察 |
|---|---|---|---|---|
| 108 | 0.20 | 0.20 | +0.00 | 兩線重合 |
| 109 | 0.30 | 0.55 | +0.25 | ALT 人工訊號較高 |
| 110 | 0.40 | 0.10 | −0.30 | 最大絕對差分,負號仍是負號 |
| 111 | 0.25 | 0.35 | +0.10 | ALT 人工訊號較高 |
| 112 | 0.20 | 0.20 | +0.00 | 兩線重合 |
這裡只有 兩條原始代理軌跡 REF 和 ALT;下方的 Delta 是這兩條線相減得到的衍生面板,不是第三項獨立生物量測。
max(abs(Delta))=0.30,但被挑中的帶符號數值是 −0.30,位置 110。Day 23 對同一虛構 ID chr1:110:A>G 保存的 synthetic_score=-0.17,不是 Day 24/25 的 delta=-0.30。這兩個數字出自不同的人工教學設定,不能加總、轉成致病機率,或放在同一軸上互相比高低。本篇會把 Day 23 只當作來源背景,而不把其分數放到圖裡。
AI_STUDIO_DAY25_PROMPT.txt。SYNTHETIC 和 NOT MODEL DATA。verify_inputs()、draw_svg()、draw_html()、emit_outputs(),理解 UI 背後各步的資料與繪圖規則。AI Studio Build Mode 的前端程式不能自動當成 Python。 我們是先做介面 MVP,再用 Python 自行實作相同的行為契約。不要將真實病人資料或敏感 API Key 貼進 AI Studio 提示詞。
| Google AI Studio 上的元件 | Python Lab 對應函式 | 初學者學到什麼 |
|---|---|---|
| 人工 JSON 輸入 | load_json() |
認識資料來源與檔案格式 |
| 圖前驗證 | verify_inputs() |
每個點需對齊、相減可回算 |
| 16:9 軌跡圖與 Delta | draw_svg() |
X/Y 映射、線段、點與正負差分 |
| 同頁數值表格 | draw_html() |
每個 SVG 點能回到資料列 |
| 多種格式匯出 | emit_outputs() |
CSV/JSON/SVG/HTML 不同用途 |
本篇不是再做一次 Day 24 的模型預測,而是重用 Day 24 已保存的人工結果。教學包內準備了兩份相互匹配的 JSON:
day24_track_delta_report.json:Day 24 合成 Delta 的衍生報告。day24_synthetic_tracks.json:Day 24 原始人工 REF/ALT 數列。程式先確保兩份資料都是 TOY_NOT_GRCH38、OFFLINE_ONLY、model_prediction=false、atlas_query_performed=false,然後逐點核對位置、REF、ALT、差分及 variant_here。還會核對原始 fixture 的實際 SHA-256 是否與 Day 24 報告聲稱的一致。只要不一致,就停止,不輸出一張可能令人誤解的圖。
螢幕 SVG 的 y=0 在最上方,所以數值越大、畫得越高,需要用「畫布底部 − 資料值 × 可畫高度」。這和高中數學課常見的 Y 軸方向相反。程式另外在底部 Delta 面板畫出水平零線:正值往上,負值往下。
圖表使用 1920×1080(16:9)、背景 #E5DEDA;下方保留一些字幕空間。這是可編輯的 SVG 科普教材,不是 Google DeepMind 官方視覺化輸出。
這次直接使用 Python 3.12 標準函式庫(json、csv、hashlib、xml.etree.ElementTree 等),不需安裝 Matplotlib 或其他套件。不用 GPU、SDK、API Key;不用讀取私人 GitHub。
下載完整教學 ZIP 後,同一資料夾會有:
Day25/
├── day25_offline_locus_plot_lab.py
├── test_day25_offline_locus_plot_lab.py
├── day24_track_delta_report.json
├── day24_synthetic_tracks.json
├── AI_STUDIO_DAY25_PROMPT.txt
├── README_Day25_Locus_Track_API_Bridge_Lab.md
└── day25_output/ # 已生成的可比對範例輸出
以下是程式全文。儘管看起來較長,主要的生醫數學部分只有「核對五筆資料、算 X/Y、畫 REF/ALT、畫 Delta」;其他行是在幫新手把輸入錯誤擋下來,並留下日後可追蹤的輸出。
"""Day 25 | OFFLINE genomic-locus REF/ALT visualization lesson.
This reads Day 24's SYNTHETIC report and its original SYNTHETIC track fixture.
It does not call AlphaGenome, Atlas, Gemini, a network endpoint or a genome FASTA.
Only Python 3.12+ standard library modules are required for the student lab.
"""
from __future__ import annotations
import argparse
import csv
import hashlib
from html import escape
import json
import math
from pathlib import Path
import re
import xml.etree.ElementTree as ET
EVIDENCE_CLASS = "SYNTHETIC_EDUCATIONAL_DEMO"
MODE = "OFFLINE_ONLY"
ASSEMBLY = "TOY_NOT_GRCH38"
INPUT_SCHEMA = "day24.synthetic.refalt_delta.v1"
TRACK_SCHEMA = "day24.synthetic.refalt_tracks.v1"
FIGURE_SCHEMA = "day25.synthetic.locus_plot.v1"
VARIANT_RE = re.compile(r"(chr(?:[1-9]|1[0-9]|2[0-2]|X|Y|M)):([1-9][0-9]*):([ACGT])>([ACGT])\Z")
SHA_RE = re.compile(r"[0-9a-f]{64}\Z")
SVG_NS = "http://www.w3.org/2000/svg"
ET.register_namespace("", SVG_NS)
def load_json(path: str | Path, *, max_bytes: int = 1_000_000):
"""Read a bounded local fixture; NEVER download or deserialize pickle files."""
file = Path(path)
if not file.is_file() or file.stat().st_size > max_bytes:
raise ValueError("Local fixture missing or exceeds 1 MB")
try:
return json.loads(file.read_text(encoding="utf-8"), parse_constant=lambda s: (_ for _ in ()).throw(ValueError("Non-finite JSON is invalid")))
except (UnicodeError, json.JSONDecodeError) as exc:
raise ValueError("Invalid local UTF-8 JSON") from exc
def sha256_file(path: str | Path) -> str:
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def _exact(record, key, expected):
if key not in record or type(record[key]) is not type(expected) or record[key] != expected:
raise ValueError(f"Incorrect synthetic input contract: {key}")
def _finite_unit_number(value):
return type(value) in (float, int) and math.isfinite(value) and 0 <= value <= 1
def verify_inputs(report, track, *, track_sha=None):
"""Reject model, patient, malformed or mismatched data before plotting."""
if not isinstance(report, dict) or not isinstance(track, dict):
raise ValueError("Day24 report and track must be JSON objects")
contract = {
"schema_version": INPUT_SCHEMA, "evidence_class": EVIDENCE_CLASS,
"mode": MODE, "assembly": ASSEMBLY,
"track_label": "ARTIFICIAL_SIGNAL_PROXY",
"track_unit": "UNIT_INTERVAL_0_TO_1_TOY",
"biosample": "TOY_CELL_CONTEXT_NOT_REAL_TISSUE",
"coordinate_system": "1-based positions", "delta_definition": "ALT_proxy - REF_proxy",
"mechanism_status": "HYPOTHESIS_ONLY_NOT_VALIDATED",
"model_prediction": False, "official_atlas_data": False,
"reference_allele_verified": False, "clinical_interpretation": False,
"atlas_query_performed": False, "real_api_calls_this_run": 0,
}
for key, expected in contract.items():
_exact(report, key, expected)
track_contract = {
"schema_version": TRACK_SCHEMA, "evidence_class": EVIDENCE_CLASS,
"mode": MODE, "assembly": ASSEMBLY,
"track_label": "ARTIFICIAL_SIGNAL_PROXY",
"track_unit": "UNIT_INTERVAL_0_TO_1_TOY",
"biosample": "TOY_CELL_CONTEXT_NOT_REAL_TISSUE",
"coordinate_system": "1-based positions", "model_prediction": False,
"official_atlas_data": False, "real_api_calls_this_run": 0,
}
for key, expected in track_contract.items():
_exact(track, key, expected)
variant_id = report.get("variant_id")
m = VARIANT_RE.fullmatch(variant_id) if isinstance(variant_id, str) else None
if not m or m.group(3) == m.group(4) or track.get("variant_id") != variant_id:
raise ValueError("Invalid or mismatched synthetic SNV")
variant_pos = int(m.group(2))
rows = report.get("point_rows")
pos, refs, alts = (track.get(k) for k in ("positions_1based", "ref", "alt"))
if not all(isinstance(v, list) for v in (rows, pos, refs, alts)):
raise ValueError("Point arrays must be lists")
if not (2 <= len(rows) <= 1000 and len(rows) == len(pos) == len(refs) == len(alts)):
raise ValueError("Point arrays must be equally sized, 2-1000 points")
if type(report.get("point_count")) is not int or report["point_count"] != len(rows):
raise ValueError("Day24 point_count is incorrect")
for i, (row, p, ref, alt) in enumerate(zip(rows, pos, refs, alts)):
if not isinstance(row, dict) or type(p) is not int or p < 1:
raise ValueError("Malformed position")
if i and p != pos[i - 1] + 1:
raise ValueError("Nonconsecutive 1-based positions")
if not (_finite_unit_number(ref) and _finite_unit_number(alt)):
raise ValueError("Invalid or non-finite proxy value")
expected = {
"position_1based": p, "ref_proxy": ref, "alt_proxy": alt,
"delta_alt_minus_ref": round(alt - ref, 3),
"variant_here": p == variant_pos,
}
for k, v in expected.items():
if k not in row or type(row[k]) is not type(v) or row[k] != v:
# int vs float is allowed for scientifically identical proxy values only
if k in ("ref_proxy", "alt_proxy", "delta_alt_minus_ref") and _finite_unit_number(row.get(k)) and abs(row[k] - v) <= 1e-9:
continue
raise ValueError(f"Day24 row {i} does not match fixture: {k}")
if not (pos[0] <= variant_pos <= pos[-1]):
raise ValueError("Variant outside plotted interval")
peak_i = max(range(len(rows)), key=lambda i: abs(rows[i]["delta_alt_minus_ref"]))
_exact(report, "largest_absolute_delta_position_1based", pos[peak_i])
if type(report.get("largest_absolute_delta")) not in (float, int) or report["largest_absolute_delta"] != rows[peak_i]["delta_alt_minus_ref"]:
raise ValueError("Largest signed delta incorrectly recorded")
provenance = report.get("input_provenance")
if not isinstance(provenance, dict) or not isinstance(provenance.get("track_fixture"), dict):
raise ValueError("Day24 provenance required")
reference = provenance["track_fixture"]
if not isinstance(reference.get("filename"), str) or not SHA_RE.fullmatch(str(reference.get("sha256"))):
raise ValueError("Malformed upstream track hash")
if track_sha is not None and track_sha != reference["sha256"]:
raise ValueError("Source track SHA-256 does not match Day24 report")
# The score from Day23 is a different proxy. It is not plotted or mixed.
link = report.get("day23_annotation_link")
if link is not None:
if not isinstance(link, dict) or link.get("variant_id") != variant_id or link.get("qc_status") != "SELECTED" or link.get("review_status") != "REVIEW_REQUIRED":
raise ValueError("Inconsistent Day23 link")
if link.get("score_status") not in ("MISSING_SCORE", "SYNTHETIC_SCORE"):
raise ValueError("Day23 status invalid")
if link["score_status"] == "MISSING_SCORE" and link.get("synthetic_score") is not None:
raise ValueError("Day23 missing score must remain null")
if link["score_status"] == "SYNTHETIC_SCORE" and (type(link.get("synthetic_score")) not in (int, float) or not math.isfinite(link["synthetic_score"])):
raise ValueError("Day23 synthetic score must be finite")
return report
def make_figure_summary(report, *, report_sha: str, track_sha: str):
verify_inputs(report, {
"schema_version": TRACK_SCHEMA, "evidence_class": EVIDENCE_CLASS,
"mode": MODE, "assembly": ASSEMBLY,
"track_label": report["track_label"], "track_unit": report["track_unit"],
"biosample": report["biosample"], "coordinate_system": report["coordinate_system"],
"variant_id": report["variant_id"],
"positions_1based": [r["position_1based"] for r in report["point_rows"]],
"ref": [r["ref_proxy"] for r in report["point_rows"]],
"alt": [r["alt_proxy"] for r in report["point_rows"]],
"model_prediction": False, "official_atlas_data": False,
"real_api_calls_this_run": 0,
}, track_sha=track_sha)
return {
"schema_version": FIGURE_SCHEMA,
"evidence_class": EVIDENCE_CLASS,
"mode": MODE,
"assembly": ASSEMBLY,
"variant_id": report["variant_id"],
"coordinate_system": "1-based positions",
"track_unit": "UNIT_INTERVAL_0_TO_1_TOY",
"point_count": report["point_count"],
"track_count": 2,
"delta_definition": "ALT_proxy - REF_proxy",
"largest_absolute_delta_position_1based": report["largest_absolute_delta_position_1based"],
"largest_absolute_delta": report["largest_absolute_delta"],
"day23_linked": report.get("day23_annotation_link") is not None,
"day23_score_combined_with_delta": False,
"source_day24_report_sha256": report_sha,
"source_day24_track_sha256": track_sha,
"upstream_day23_source_sha256": (report.get("input_provenance", {}).get("day23_packet") or {}).get("sha256"),
"upstream_native_model_response_retained": False,
"model_prediction": False,
"official_atlas_data": False,
"reference_allele_verified": False,
"clinical_interpretation": False,
"real_api_calls_this_run": 0,
"figure_caption": "SYNTHETIC proxy signals. Five points; not actual AlphaGenome outputs or clinical evidence.",
}
def draw_svg(report):
"""Render a single 1920×1080 16:9 figure with aligned signed-delta panel."""
rows = report["point_rows"]
variant_id = report["variant_id"]
n = len(rows)
variant_pos = int(VARIANT_RE.fullmatch(variant_id).group(2))
root = ET.Element(f"{{{SVG_NS}}}svg", {"width": "1920", "height": "1080", "viewBox": "0 0 1920 1080", "role": "img", "aria-label": "Synthetic REF ALT genomic track with aligned delta; not model predictions"})
def elem(tag, attrs=None, label=None, parent=None):
child = ET.SubElement(parent if parent is not None else root, f"{{{SVG_NS}}}{tag}", {k: str(v) for k, v in (attrs or {}).items()})
if label is not None:
child.text = str(label)
return child
def text(x, y, label, size=24, fill="#444956", weight="normal", anchor=None):
a = {"x": x, "y": y, "font-family": "Arial, sans-serif", "font-size": size, "fill": fill, "font-weight": weight}
if anchor:
a["text-anchor"] = anchor
return elem("text", a, label)
elem("rect", {"width": 1920, "height": 1080, "fill": "#E5DEDA"})
text(108, 88, "Day 25 | SYNTHETIC REF / ALT GENOMIC TRACKS", 49, "#323c4c", "bold")
text(110, 133, "Same coordinates · two proxy tracks · one signed difference", 26, "#5d6570")
elem("rect", {"x": 110, "y": 162, "width": 1700, "height": 66, "rx": 18, "fill": "#f6f0ed"})
text(140, 205, variant_id, 25, "#4d5d78", "bold")
text(530, 205, "TOY_NOT_GRCH38 · 1-based · artificial units 0–1", 23, "#65707e")
text(1530, 205, "NOT MODEL DATA", 22, "#9d4655", "bold")
elem("rect", {"x": 105, "y": 256, "width": 1710, "height": 392, "rx": 22, "fill": "#fffaf8"})
elem("rect", {"x": 105, "y": 668, "width": 1710, "height": 218, "rx": 22, "fill": "#fffaf8"})
text(150, 310, "REF / ALT proxy signal", 30, "#394457", "bold")
text(150, 720, "Signed Delta = ALT − REF", 30, "#394457", "bold")
left, right = 285, 1710
top, bottom = 349, 567
x = lambda i: left + (right - left) * i / (n - 1)
y = lambda v: bottom - (bottom - top) * v
for mark in (0, 0.25, 0.5, 0.75, 1.0):
yy = y(mark)
elem("line", {"x1": left, "y1": f"{yy:.2f}", "x2": right, "y2": f"{yy:.2f}", "stroke": "#e4e7e8", "stroke-width": 2})
text(245, yy + 9, f"{mark:g}", 23, "#727b84", anchor="end")
for name, field, color in (("REF", "ref_proxy", "#507d98"), ("ALT", "alt_proxy", "#c27486")):
coords = " ".join(f"{x(i):.2f},{y(row[field]):.2f}" for i, row in enumerate(rows))
elem("polyline", {"points": coords, "stroke": color, "stroke-width": 7, "stroke-linecap": "round", "stroke-linejoin": "round", "fill": "none", "data-track": name})
for i, row in enumerate(rows):
dot = elem("circle", {"cx": f"{x(i):.2f}", "cy": f"{y(row[field]):.2f}", "r": 9, "fill": color, "stroke": "#fff", "stroke-width": 2, "data-position": row["position_1based"], "data-track": name})
elem("title", label=f"{name} position {row['position_1based']}: {row[field]:g}", parent=dot)
for idx, (name, color) in enumerate((("REF", "#507d98"), ("ALT", "#c27486"))):
start = 1480 + idx * 140
elem("line", {"x1": start, "x2": start + 35, "y1": 309, "y2": 309, "stroke": color, "stroke-width": 7})
text(start + 44, 319, name, 24, color, "bold")
for i, row in enumerate(rows):
if n <= 12 or i % max(1, n // 9) == 0 or i == n - 1:
text(x(i), 612, str(row["position_1based"]), 24, "#505d6a", anchor="middle")
pos_i = next(i for i, r in enumerate(rows) if r["position_1based"] == variant_pos)
elem("line", {"x1": f"{x(pos_i):.2f}", "y1": 324, "x2": f"{x(pos_i):.2f}", "y2": 853, "stroke": "#a18458", "stroke-width": 3, "stroke-dasharray": "10,9", "data-variant-marker": str(variant_pos)})
text(x(pos_i) + 16, 348, "Variant", 21, "#907248", "bold")
deltas = [r["delta_alt_minus_ref"] for r in rows]
scale = max(max(abs(v) for v in deltas), .1)
zero = 784
elem("line", {"x1": left, "y1": zero, "x2": right, "y2": zero, "stroke": "#9ea6a7", "stroke-width": 2.5})
for i, (row, d) in enumerate(zip(rows, deltas)):
e = zero - d / scale * 65
if d:
elem("line", {"x1": f"{x(i):.2f}", "y1": zero, "x2": f"{x(i):.2f}", "y2": f"{e:.2f}", "stroke": "#937993", "stroke-width": 13, "stroke-linecap": "round", "data-delta-pos": str(row["position_1based"])})
text(x(i), 861, f"{d:+.2f}", 22, "#5d5472", anchor="middle")
p = report["largest_absolute_delta_position_1based"]
d = report["largest_absolute_delta"]
text(110, 926, f"Largest absolute Delta at 1-based {p}: signed value {d:+.2f}", 27, "#384459", "bold")
text(110, 968, "SYNTHETIC EDUCATIONAL DATA | No Atlas API, no genome reference validation, no clinical inference", 23, "#884f5a")
# Deliberately leave a clear lower margin for future video captions.
return ET.tostring(root, encoding="unicode")
def draw_html(report, summary, svg):
"""Create browser-readable offline lesson with SVG and fully escaped text."""
h = lambda t: escape(str(t), quote=True)
rows = "\n".join("<tr>" + "".join(f"<td>{h(r[f])}</td>" for f in ("position_1based", "ref_proxy", "alt_proxy", "delta_alt_minus_ref", "variant_here")) + "</tr>" for r in report["point_rows"])
return f'''<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Day 25 | 合成基因座 REF/ALT 軌跡</title>
<style>body{{margin:0;background:#E5DEDA;color:#293545;font-family:system-ui, sans-serif}}main{{max-width:1240px;margin:auto;padding:25px}}.card{{background:#fffaf8;border-radius:20px;padding:22px;margin:18px 0}}svg{{width:100%;height:auto;border-radius:12px}}table{{border-collapse:collapse;width:100%}}td,th{{padding:10px;border-bottom:1px solid #e2d4d0;text-align:right}}td:first-child,th:first-child{{text-align:left}}.warning{{color:#873a51;font-weight:700}}.nav a{{margin-right:16px;color:#35587b}}</style></head>
<body><main><h1>Day 25 | 合成基因座軌跡圖</h1><p>REF 與 ALT 使用共同 1-based 位置;下方 Delta = ALT − REF,負號不可消失。</p>
<div class="card">{svg}</div>
<div class="card"><h2>逐點數值核對</h2><table><thead><tr><th>位置 1-based</th><th>REF proxy</th><th>ALT proxy</th><th>ALT − REF</th><th>變異位置?</th></tr></thead><tbody>{rows}</tbody></table></div>
<div class="card"><h2>證據說明</h2><p class="warning">SYNTHETIC_EDUCATIONAL_DEMO · TOY_NOT_GRCH38 · 0 real API calls</p>
<p>這是 Day 24 人工 JSON 的繪圖,不是 AlphaGenome Atlas/Model 的預測結果。Day 23 的人工分數不會拿來與此 Delta 加總。</p>
<p>Day24 report SHA-256: <code>{h(summary['source_day24_report_sha256'])}</code></p>
<p>Day24 track SHA-256: <code>{h(summary['source_day24_track_sha256'])}</code></p>
<p>模型機制:NOT_VALIDATED · 人類審閱:REQUIRED · 臨床解讀:不適用</p>
<p class="nav"><a href="day25_locus_track.csv">CSV 數據表</a><a href="day25_locus_summary.json">JSON 概要</a><a href="day25_locus_tracks.svg">SVG 圖</a></p></div></main></body></html>'''
def emit_outputs(report_path, source_track_path, output_dir):
report = load_json(report_path)
track = load_json(source_track_path)
track_sha = sha256_file(source_track_path)
verify_inputs(report, track, track_sha=track_sha)
summary = make_figure_summary(report, report_sha=sha256_file(report_path), track_sha=track_sha)
svg = draw_svg(report)
folder = Path(output_dir)
folder.mkdir(parents=True, exist_ok=True)
json_path = folder / "day25_locus_summary.json"
csv_path = folder / "day25_locus_track.csv"
svg_path = folder / "day25_locus_tracks.svg"
html_path = folder / "day25_locus_view.html"
json_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
with csv_path.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=["position_1based", "ref_proxy", "alt_proxy", "delta_alt_minus_ref", "variant_here"])
writer.writeheader()
writer.writerows(report["point_rows"])
svg_path.write_text(svg, encoding="utf-8")
html_path.write_text(draw_html(report, summary, svg), encoding="utf-8")
return {
"status": "PASS", "mode": MODE, "evidence_class": EVIDENCE_CLASS,
"variant_id": summary["variant_id"],
"point_count": summary["point_count"], "track_count": 2,
"largest_absolute_delta_position_1based": summary["largest_absolute_delta_position_1based"],
"largest_absolute_delta": summary["largest_absolute_delta"],
"json_file": str(json_path), "csv_file": str(csv_path),
"svg_file": str(svg_path), "html_file": str(html_path),
"real_api_calls_this_run": 0,
}
def main(argv=None):
parser = argparse.ArgumentParser(description="Draw 16:9 synthetic REF/ALT locus tracks from Day24")
parser.add_argument("--mode", choices=["offline"], required=True)
parser.add_argument("--day24-report", default="day24_track_delta_report.json")
parser.add_argument("--source-track", default="day24_synthetic_tracks.json")
parser.add_argument("--output-dir", default="day25_output")
args = parser.parse_args(argv)
try:
result = emit_outputs(args.day24_report, args.source_track, args.output_dir)
except (ValueError, KeyError, OSError, TypeError) as exc:
parser.error(str(exc))
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
return result
if __name__ == "__main__":
main()
先將 AlphaGenome_Day25_Locus_Track_API_Bridge_Author_Review_Pack.zip 下載至 Windows Downloads。在 WSL Ubuntu 終端機貼上以下 Bash 指令(不要貼到 Python 互動提示符):
cd /mnt/c/Users/user/Downloads
mkdir -p ~/alphagenome-day25-qa
python3.12 -m zipfile -e \
AlphaGenome_Day25_Locus_Track_API_Bridge_Author_Review_Pack.zip \
~/alphagenome-day25-qa
cd ~/alphagenome-day25-qa
# 先測試,確保程式能發現錯位或錯誤資料
python3.12 -m unittest -v test_day25_offline_locus_plot_lab
# 再使用本 ZIP 已準備的 Day24 人工資料產圖
python3.12 day25_offline_locus_plot_lab.py \
--mode offline \
--day24-report day24_track_delta_report.json \
--source-track day24_synthetic_tracks.json \
--output-dir day25_output
預期單元測試結尾為:
Ran 52 tests
OK
這是本次打包環境的測試結果,使用者仍須在 WSL Python 3.12 自行執行確認。
下方是本次執行的完整 stdout(單行 JSON,不是隨手填的想像回應),使用的是人工 Day24 來源:
{"csv_file": "day25_output/day25_locus_track.csv", "evidence_class": "SYNTHETIC_EDUCATIONAL_DEMO", "html_file": "day25_output/day25_locus_view.html", "json_file": "day25_output/day25_locus_summary.json", "largest_absolute_delta": -0.3, "largest_absolute_delta_position_1based": 110, "mode": "OFFLINE_ONLY", "point_count": 5, "real_api_calls_this_run": 0, "status": "PASS", "svg_file": "day25_output/day25_locus_tracks.svg", "track_count": 2, "variant_id": "chr1:110:A>G"}
執行後, day25_output/ 會產生:
| 檔案 | 初學者用來做什麼 |
|---|---|
day25_locus_summary.json |
讀取來源 SHA、變異座標、差分及證據標籤 |
day25_locus_track.csv |
使用 Excel 或 Python 核對每個位置的 REF、ALT、Delta |
day25_locus_tracks.svg |
用瀏覽器或向量編輯器打開 1920×1080 圖 |
day25_locus_view.html |
單一 HTML 頁面,直接檢查 SVG 與五筆表格 |
教學包還預先附帶 day25_locus_preview.png,方便你在 Notion 先預覽;這張 PNG 是在打包環境額外由 CairoSVG 轉出,不代表主程式依賴 CairoSVG。
Windows 檔案總管路徑:
\\wsl.localhost\Ubuntu-24.04\home\user\alphagenome-day25-qa\day25_output
對照上面的五筆表格:
SYNTHETIC、TOY_NOT_GRCH38 與 NOT MODEL DATA 等標示。day24_track_delta_report.json 的 point_rows,並在 CSV 對到同樣五個點。delta=ALT−REF,最大絕對差分 0.30 的帶符號數值是 −0.30。TOY_NOT_GRCH38、model_prediction=false、real_api_calls_this_run=0。本篇的 REF、ALT、Delta、變異 ID、坐標與細胞情境完全人工。SYNTHETIC_EDUCATIONAL_DEMO 只驗證資料契約與 SVG 可重現,不是 AlphaGenome Atlas 或 AlphaGenome Model API 的真實輸出,也不是分子機制、剪接、基因表現、臨床診斷或濕實驗證據。
真正做 REF/ALT 預測時,還必須記錄基因組組裝版本、參考鹼基、轉錄本、模型 API 輸出類型、細胞 ontology、單位、版本與來源證據。當前的 Day 24 人工資料不提供這些真實科學資訊,不能只因圖畫得漂亮就替它補寫。
已有的 Reboot 歷史 LIVE 模型預測是另一份獨立資料,本篇沒有讀取、混用或執行它;也沒有重新觸發 API。若日後加入真實軌跡,必須另案核對其輸出契約,並取得作者明確授權。
軟體 QA 與科學驗證不是同一件事。 單元測試 PASS 只表示人工資料上的軟體行為符合規格。
明天 Day 26,我們會從長條軌跡改看 DNA 短序列的 Motif:用人工機率矩陣理解「字母改變,序列偏好分數怎樣變化」,並分辨 PPM-like 代理算術、真正的 log-odds PWM 與轉錄因子結合實驗。